package weather_service.weatherprovider;

import org.jdom.Document;
import org.jdom.Element;
import org.apache.commons.lang.Validate;
import weather_service.weatherprovider.weatherdatamodel.*;
import weather_service.weatherprovider.locationdatamodel.SearchLocations;
import weather_service.weatherprovider.locationdatamodel.SearchLocation;

import java.util.Iterator;

/**
 * Parser for WeatherChannel 2.0 weather service. This class parses responses for "local" queries only and not
 * for: <p>
 * <ol>
 * <li>"search" responses (for location lookups)</li>
 * <li>"error" responses (in response to "local")</li>
 * </ol>
 * <p/>
 */
public class Weather_20_XMLParser {
  //:::::::::::::::::::::::::::::::::::::::::::::::
  // "local" constants
  //:::::::::::::::::::::::::::::::::::::::::::::::
  // weather elements
  public static final String WEATHER_ROOT = "weather";
  public static final String VERSION = "ver";

  //weather response document version from Weather.com
  public static final String WEATHER_RESPONSE_VERSION = "2.0";

  // header elements
  public static final String HEAD = "head";
  public static final String LOCALE = "locale";
  public static final String FORM = "form";
  public static final String TEMPERATURE_UNIT = "ut";
  public static final String DISTANCE_UNIT = "ud";
  public static final String SPEED_UNIT = "us";
  public static final String PRECIPITATION_UNIT = "up";
  public static final String PRESSURE_UNIT = "ur";

  // location elements
  public static final String LOCATION = "loc";
  public static final String LOCATION_ID = "id";
  public static final String DISPLAY_NAME = "dnam";
  public static final String LOCAL_TIME = "tm";
  public static final String LATITUDE = "lat";
  public static final String LONGITUDE = "lon";
  public static final String SUNRISE_TIME = "sunr";
  public static final String SUNSET_TIME = "suns";
  public static final String TIMEZONE = "zone";

  // current conditions elements
  public static final String CURRENT_CONDITIONS = "cc";
  public static final String LAST_UPDATED = "lsup";
  public static final String OBSERVATION_STATION = "obst";
  public static final String TEMPERATURE = "tmp";
  public static final String FEELS_LIKE_TEMP = "flik";
  public static final String DESCRIPTION = "t";
  public static final String ICON = "icon";

  public static final String BAROMETRIC_PRESSURE = "bar";
  public static final String PRESSURE = "r";
  public static final String PRESSURE_TREND = "d";

  public static final String WIND = "wind";
  public static final String WIND_SPEED = "s";
  public static final String WIND_GUST_SPEED = "gust";
  public static final String WIND_DEGREES = "d";
  public static final String WIND_DIRECTION = "t";

  public static final String HUMIDITY = "hmid";
  public static final String VISIBILITY = "vis";

  public static final String UV = "uv";
  public static final String UV_INDEX = "i";
  public static final String UV_DESCRIPTION = "t";

  public static final String DEWPOINT = "dewp";

  // forecast elements
  public static final String FORECAST = "dayf";
  public static final String DAY = "day";
  public static final String DAY_PART = "part";
  public static final String DAY_OR_NIGHT = "p";
  public static final String DAY_PART_DAY = "d";
  public static final String DAY_PART_NIGHT = "n";
  public static final String HIGH_TEMP = "hi";
  public static final String LOW_TEMP = "low";
  public static final String PRECIPITATION_CHANCE = "ppcp";
  public static final String DAY_INDEX = "d";
  public static final String DAY_OF_WEEK = "t";
  public static final String DAY_DATE = "dt";

  //:::::::::::::::::::::::::::::::::::::::::::::::
  // error constants
  //:::::::::::::::::::::::::::::::::::::::::::::::
  // error elements
  public static final String ERROR_ROOT = "error";
  public static final String ERROR = "err";
  public static final String ERROR_TYPE_ATTRIB = "type";

  public static final String NO_ERROR_TYPE_SET = "-1";
  public static final String UNKNOWN_ERROR = "0";
  public static final String NO_LOCATION_PROVIDED_ERROR = "1";
  public static final String INVALID_LOCATION_PROVIDED_ERROR = "2";
  public static final String INVALID_PARTNERID_ERROR = "100";
  public static final String INVALID_PRODUCT_CODE = "101";
  public static final String INVALID_LICENSE_KEY = "102";

  //:::::::::::::::::::::::::::::::::::::::::::::::
  // "search" constants
  //:::::::::::::::::::::::::::::::::::::::::::::::
  public static final String SEARCH_ROOT = "search";
  public static final String SEARCH_VERSION_ATTRIBUTE = "ver";
  public static final String SEARCH_VERSION_ATTRIBUTE_VALUE = "2.0";
  public static final String SEARCH_LOCATION_ELEMENT = "loc";
  public static final String SEARCH_LOCATION_ATTRIBUTE_ID = "id";
  public static final String SEARCH_LOCATION_ATTRIBUTE_TYPE = "type";

  //:::::::::::::::::::::::::::::::::::::::::::::::
  // data members
  //:::::::::::::::::::::::::::::::::::::::::::::::
  private String _type;

  //:::::::::::::::::::::::::::::::::::::::::::::::
  // constructor
  //:::::::::::::::::::::::::::::::::::::::::::::::
  public Weather_20_XMLParser() {
    _type = "weather_2.0";
  }

  //:::::::::::::::::::::::::::::::::::::::::::::::
  // "search" service
  //:::::::::::::::::::::::::::::::::::::::::::::::
  /**
   *
   * @param document
   * @return the SearchLocations object may be empty, so be sure to check the size before using it
   * @throws WeatherError
   * @throws NullPointerException - if some of the elements or attributes are not found, then this exception can be thrown,
   *         which implies there is a parsing issue (which will be thrown by the caller of this method)
   */
  protected SearchLocations parseSearchQuery(Document document) throws WeatherError, NullPointerException {
    SearchLocations locations = new SearchLocations();

    //iterate through the document to see if there are any search locations.
    Element searchRoot = document.getRootElement();

    if (searchRoot.getName().equals(ERROR_ROOT)) {//check if theres an error element
      parseError(searchRoot);
      return locations; //this should not execute, since an error was reported...
    }
    else if (!searchRoot.getName().equals(SEARCH_ROOT)) {//check if a search element root is provided
      throw new WeatherError(WeatherError.ErrorTypes.SEARCH_LOCATIONS_PARSING_ERROR, "root element in server response to search query is missing root element:"+SEARCH_ROOT);
    }

    String searchVersion = searchRoot.getAttributeValue(SEARCH_VERSION_ATTRIBUTE) == null ? "" : searchRoot.getAttributeValue(SEARCH_VERSION_ATTRIBUTE);
    if (!searchVersion.equals(SEARCH_VERSION_ATTRIBUTE_VALUE)) throw new WeatherError( WeatherError.ErrorTypes.INVALID_SEARCH_VERSION , "the search query returned a response version ("+searchVersion+"), that is not the correct version expected ("+SEARCH_VERSION_ATTRIBUTE_VALUE+")");

    //iterate through the "loc" child elements, if any
    Iterator locs = searchRoot.getChildren(SEARCH_LOCATION_ELEMENT).iterator();
    while (locs.hasNext()) {
      Element locElement = (Element) locs.next();
      locations.add(parseSearchLocation(locElement));
    }

    return locations;
  }

  protected SearchLocation parseSearchLocation( Element locationElement  ) {
    SearchLocation location = new SearchLocation();

    location.setId( locationElement.getAttributeValue(SEARCH_LOCATION_ATTRIBUTE_ID) == null ? "" : locationElement.getAttributeValue(SEARCH_LOCATION_ATTRIBUTE_ID) );
    location.setType( locationElement.getAttributeValue(SEARCH_LOCATION_ATTRIBUTE_TYPE) == null ? "" : locationElement.getAttributeValue(SEARCH_LOCATION_ATTRIBUTE_TYPE) );
    location.setName( locationElement.getText() == null ? "" : locationElement.getText() );

    return location;
  }

  //:::::::::::::::::::::::::::::::::::::::::::::::
  // "local" service
  //:::::::::::::::::::::::::::::::::::::::::::::::
  /**
   * run this method only on documents returned by the "local" method (of Weather.com). if you run this on the
   * "search" responses or "error" responses, it will not work!
   *
   * @param document - only pass it documents that contain responses from the "local" method; no not pass "error" or "search" responses
   * @return
   * @throws WeatherError - if there is some problem parsing the document then it throws a WeatherError.WEATHER_PARSING_ERROR
   */
  public boolean isMyType(Document document) throws WeatherError{
    try{

      Validate.notNull( document , "document is null");

      Element root = document.getRootElement();
      Validate.notNull( root , "document does not have a root element");

      String rootName = root.getName();

      //assert rootName is not null
      Validate.isTrue( rootName.equals(WEATHER_ROOT) , "isMyType can only be applied to XML document returned by a call to weather method, invalid document root is provided" );

      String rootVersion = root.getAttributeValue(VERSION)==null ? "" : root.getAttributeValue(VERSION);

      if (rootName.equals(WEATHER_ROOT) && rootVersion.equals(WEATHER_RESPONSE_VERSION)) return true;
      else return false;

    } catch(Exception e){
      throw new WeatherError( WeatherError.ErrorTypes.WEATHER_PARSING_ERROR , e );
    }

  }

  /**
   * this method actually parses the "local" query's response document into a WeatherReport object.
   * @param document
   * @param validate
   * @return
   * @throws WeatherError
   */
  public WeatherReport parseWeatherQuery(Document document, boolean validate) throws WeatherError {
    /**
    if (validate) {
      validateFeed(document);
    }
    */
    Element root = document.getRootElement();

    parseError(root);

    if (isMyType(document)) return parseWeatherResponse(root);
    else throw new WeatherError( WeatherError.ErrorTypes.INVALID_WEATHER_REPORT_VERSION , "weather query response from Weather.com is a different version than what this parser can handle");
  }

  /*
  protected void validateFeed(Document document) throws Exception {
    // TODO - there actually is a DTD for this, so it might validate.
    if (false)
      throw new Exception("This should never throw.");
  }
  */

  public String getType() {
    return _type;
  }

  /**
   * Parses the root element of a JDOM document into a WeatherChannel bean.
   * No exceptions are thrown - these methods try their best to fill the bean up with data, and just leave
   * properties empty when they can't.
   *
   * @param root the root element of the JDOM document to parse.
   * @return the parsed WeatherChannel bean.
   */
  protected WeatherReport parseWeatherResponse(Element root) {
    WeatherReport report = new WeatherReport(getType());

    report.setHeader(parseHeader(root));

    report.setLocation(parseLocation(root));

    report.setCurrentConditions(parseCurrentConditions(root));

    report.setForecast(parseForecast(root));

    return report;
  }

  /**
   * Parses the root element of a WeatherChannel looking for the Header.
   *
   * @param weatherRoot the root element of the document to parse for the Header.
   * @return the parsed Header bean.
   */
  protected Header parseHeader(Element weatherRoot) {
    Header header = null;
    Element head = weatherRoot.getChild(HEAD);

    if (head != null) {
      header = new Header();

      Element e = head.getChild(LOCALE);
      if (e != null) {
        header.setLocale(e.getText());
      }
      e = head.getChild(FORM);
      if (e != null) {
        header.setForm(e.getText());
      }
      e = head.getChild(TEMPERATURE_UNIT);
      if (e != null) {
        header.setTemperatureUnit(e.getText());
      }
      e = head.getChild(DISTANCE_UNIT);
      if (e != null) {
        header.setDistanceUnit(e.getText());
      }
      e = head.getChild(SPEED_UNIT);
      if (e != null) {
        header.setSpeedUnit(e.getText());
      }
      e = head.getChild(PRECIPITATION_UNIT);
      if (e != null) {
        header.setPrecipitationUnit(e.getText());
      }
      e = head.getChild(PRESSURE_UNIT);
      if (e != null) {
        header.setPressureUnit(e.getText());
      }
    }
    return header;
  }

  /**
   * Parses the root element of a WeatherChannel looking for the Location.
   *
   * @param weatherRoot the root element of the document to parse for the Location.
   * @return the parsed Location bean.
   */
  protected Location parseLocation(Element weatherRoot) {
    Location location = null;
    Element loc = weatherRoot.getChild(LOCATION);

    if (loc != null) {
      location = new Location();

      String loc_id = loc.getAttributeValue(LOCATION_ID);
      location.setLocationId(loc_id == null ? "N/A" : loc_id);

      Element e = loc.getChild(DISPLAY_NAME);
      if (e != null) {
        location.setDisplayName(e.getText());
      }
      e = loc.getChild(LOCAL_TIME);
      if (e != null) {
        location.setLocalTime(e.getText());
      }
      e = loc.getChild(LATITUDE);
      if (e != null) {
        location.setLatitude(Double.parseDouble(e.getText()));
      }
      e = loc.getChild(LONGITUDE);
      if (e != null) {
        location.setLongitude(Double.parseDouble(e.getText()));
      }
      e = loc.getChild(SUNRISE_TIME);
      if (e != null) {
        location.setSunriseTime(e.getText());
      }
      e = loc.getChild(SUNSET_TIME);
      if (e != null) {
        location.setSunsetTime(e.getText());
      }
      e = loc.getChild(TIMEZONE);
      if (e != null) {
        location.setTimeZone(e.getText());
      }
    }
    return location;
  }

  /**
   * Parses the root element of a WeatherChannel looking for the Forecast.
   *
   * @param weatherRoot the root element of the document to parse for the Forecast.
   * @return the parsed Forecast bean.
   */
  protected Forecast parseForecast(Element weatherRoot) {
    Forecast forecast = null;
    Element dailyForecast = weatherRoot.getChild(FORECAST);

    if (dailyForecast != null) {
      forecast = new Forecast();

      Element e = dailyForecast.getChild(LAST_UPDATED);
      if (e != null) {
        forecast.setLastUpdated(e.getText());
      }

      Iterator days = dailyForecast.getChildren(DAY).iterator();
      while (days.hasNext()) {
        Element dayRoot = (Element) days.next();
        forecast.addDay(parseDay(dayRoot));
      }
    }
    return forecast;
  }

  /**
   * Parses the root element of a WeatherChannel looking for Current Conditions.
   *
   * @param root the root element of the document to parse for Current Conditions.
   * @return the parsed CurrentConditions bean.
   */
  protected CurrentConditions parseCurrentConditions(Element root) {
    CurrentConditions conditions = null;
    Element cc = root.getChild(CURRENT_CONDITIONS);

    if (cc != null) {
      conditions = new CurrentConditions();

      Element e = cc.getChild(LAST_UPDATED);
      if (e != null) {
        conditions.setLastUpdated(e.getText());
      }
      e = cc.getChild(OBSERVATION_STATION);
      if (e != null) {
        conditions.setObservationStation(e.getText());
      }
      e = cc.getChild(TEMPERATURE);
      if (e != null) {
        conditions.setTemperature(e.getText());
      }
      e = cc.getChild(FEELS_LIKE_TEMP);
      if (e != null) {
        conditions.setFeelsLikeTemperature(e.getText());
      }
      e = cc.getChild(DESCRIPTION);
      if (e != null) {
        conditions.setDescription(e.getText());
      }
      e = cc.getChild(ICON);
      if (e != null) {
        conditions.setIcon(e.getText());
      }
      e = cc.getChild(HUMIDITY);
      if (e != null) {
        conditions.setHumidity(e.getText());
      }
      e = cc.getChild(VISIBILITY);
      if (e != null) {
        conditions.setVisibility(e.getText());
      }
      e = cc.getChild(DEWPOINT);
      if (e != null) {
        conditions.setDewPoint(e.getText());
      }

      e = cc.getChild(BAROMETRIC_PRESSURE);
      if (e != null) {
        conditions.setBarometricPressure(parseBarometricPressure(e));
      }
      e = cc.getChild(WIND);
      if (e != null) {
        conditions.setWind(parseWind(e));
      }
      e = cc.getChild(UV);
      if (e != null) {
        conditions.setUv(parseUV(e));
      }
    }
    return conditions;
  }

  protected Day parseDay(Element dayRoot) {
    Day day = new Day();

    String attrib = dayRoot.getAttributeValue(DAY_INDEX);
    if (attrib != null) {
      day.setDayIndex(attrib);
    }
    attrib = dayRoot.getAttributeValue(DAY_OF_WEEK);
    if (attrib != null) {
      day.setDayName(attrib);
    }
    attrib = dayRoot.getAttributeValue(DAY_DATE);
    if (attrib != null) {
      day.setDayDate(attrib);
    }

    Element e = dayRoot.getChild(HIGH_TEMP);
    if (e != null) {
      day.setHigh(e.getText());
    }
    e = dayRoot.getChild(LOW_TEMP);
    if (e != null) {
      day.setLow(e.getText());
    }
    e = dayRoot.getChild(SUNRISE_TIME);
    if (e != null) {
      day.setSunriseTime(e.getText());
    }
    e = dayRoot.getChild(SUNSET_TIME);
    if (e != null) {
      day.setSunsetTime(e.getText());
    }

    Iterator it = dayRoot.getChildren(DAY_PART).iterator();
    while (it.hasNext()) {
      Element dayPartRoot = (Element) it.next();
      day.addPart(parseDayPart(dayPartRoot));
    }
    return day;
  }

  protected DayPart parseDayPart(Element dayPartRoot) {
    DayPart dayPart = new DayPart();

    String attrib = dayPartRoot.getAttributeValue(DAY_OR_NIGHT);
    if (attrib != null) {
      dayPart.setPartId(attrib);
    }

    Element e = dayPartRoot.getChild(ICON);
    if (e != null) {
      dayPart.setIcon(e.getText());
    }
    e = dayPartRoot.getChild(DESCRIPTION);
    if (e != null) {
      dayPart.setDescription(e.getText());
    }
    e = dayPartRoot.getChild(PRECIPITATION_CHANCE);
    if (e != null) {
      dayPart.setPrecipitation(e.getText());
    }
    e = dayPartRoot.getChild(HUMIDITY);
    if (e != null) {
      dayPart.setHumidity(e.getText());
    }
    e = dayPartRoot.getChild(WIND);
    if (e != null) {
      dayPart.setWind(parseWind(e));
    }
    return dayPart;
  }

  protected Wind parseWind(Element windRoot) {
    Wind wind = new Wind();

    Element e = windRoot.getChild(WIND_SPEED);
    if (e != null) {
      wind.setSpeed(e.getText());
    }
    e = windRoot.getChild(WIND_GUST_SPEED);
    if (e != null) {
      wind.setGust(e.getText());
    }
    e = windRoot.getChild(WIND_DEGREES);
    if (e != null) {
      wind.setDegrees(e.getText());
    }
    e = windRoot.getChild(WIND_DIRECTION);
    if (e != null) {
      wind.setDirection(e.getText());
    }
    return wind;
  }

  protected BarometricPressure parseBarometricPressure(Element root) {
    BarometricPressure bp = new BarometricPressure();

    Element e = root.getChild(PRESSURE);
    if (e != null) {
      bp.setPressure(e.getText());
    }
    e = root.getChild(PRESSURE_TREND);
    if (e != null) {
      bp.setTrend(e.getText());
    }
    return bp;
  }

  protected UltraViolet parseUV(Element root) {
    UltraViolet uv = new UltraViolet();

    Element e = root.getChild(UV_INDEX);
    if (e != null) {
      uv.setIndex(e.getText());
    }
    e = root.getChild(UV_DESCRIPTION);
    if (e != null) {
      uv.setDescription(e.getText());
    }
    return uv;
  }

  /**
   * In case of an error, throw WeatherError exception. This reports application exceptions, not protocol exceptions.
   * Application exceptions are: parsing exceptions, or error conditions reported by weather.com
   *
   * @param root the root element of the document to parse for the weather.
   * @throws WeatherError - this exception is thrown if a problem is found while parsing the document
   */
  protected void parseError(Element root) throws WeatherError {
    Element error = root.getChild(ERROR);

    if (error != null) {
      //check the attribute ERROR_TYPE_ATTRIB
      String errorType = error.getAttributeValue( ERROR_TYPE_ATTRIB ) == null ? NO_ERROR_TYPE_SET : error.getAttributeValue( ERROR_TYPE_ATTRIB );
      String errorValue = error.getText() == null ? "" : error.getText();

      //fill out error conditons...
      throw new WeatherError( errorType , errorValue );
    }
  }//parseError


}//end Weather20Parser

